<!-- file-auto-create  -->
<template>
  <el-form ref="ruleForm"
           :model="ruleForm"
           :label-width="labelWidth"
           :class="modeType == 'detail' ? 'detail-mode-type' : 'edit-mode-type'"
           class="components-myForm-wrap dst-add-item-box"
           @submit.native.prevent>
    <template v-cloak>
      <template v-for="(item,index) in dataArr">
        <!-- 做分隔符作用 -->
        <template v-if="item.type =='cutApart' && !item.isHidden">
          <div :key="index"
               :style="calcDefaultOption(item,'style', {})"
               :class="['cutApart', item.class || '']" />
        </template>
        <!-- 不需要label样式，插入一个元素 -->
        <template v-else-if="item.type =='slotMemberItem' && !item.isHidden">
          <slot :name="item.prop"
                :form="ruleForm[item.prop]" />
        </template>
        <!-- 不需要label样式，自己render一个元素 -->
        <template v-else-if="item.type =='renderMemberItem' && !item.isHidden">
          <render :key="index"
                  :render="item.render"
                  :form="ruleForm"
                  :item="item" />
        </template>
        <!-- 需要label -->
        <!-- 渲染右边内容 -->
        <!-- 详情模式 -->
        <!-- 详情模式 -->
        <template v-else>
          <template v-if="modeType == 'detail'">
            <DstFormDetail v-if="!item.isHidden"
                           ref="DstFormDetail"
                           :key="index"
                           :item="item"
                           :rule-form="ruleForm"
                           :index="index"
                           @itemClick="itemClick">
              <!-- 父级中得slot传递到子级中。如果有hasOther时也需传入。但非常不推荐使用slot。嵌套传入slot让代码不便维护。应尽量使用render -->
              <slot :slot="item.prop"
                    :name="item.prop"
                    :form="ruleForm" />
              <slot v-if="item.hasOther && Object.prototype.toString.call(item.hasOther) === '[object Object]' && item.hasOther.prop"
                    :slot="item.hasOther.prop"
                    :name="item.hasOther.prop"
                    :form="ruleForm" />
            </DstFormDetail>
          </template>
          <!-- 非详情模式 -->
          <!-- 非详情模式 -->
          <template v-else>
            <DstFormAdd v-if="!item.isHidden"
                        ref="DstFormAdd"
                        :key="index"
                        :item="item"
                        :rule-form="ruleForm"
                        :index="index"
                        @cascaderItemChange="cascaderItemChange"
                        @selectCascaderChange="selectCascaderChange"
                        @selectItemChange="selectItemChange"
                        @itemChange="itemChange"
                        @itemClick="itemClick">
              <!-- 父级中得slot传递到子级中。如果有hasOther时也需传入。但非常不推荐使用slot。嵌套传入slot让代码不便维护。应尽量使用render -->
              <slot :slot="item.prop"
                    :name="item.prop"
                    :form="ruleForm" />
              <slot v-if="item.hasOther && Object.prototype.toString.call(item.hasOther) === '[object Object]' && item.hasOther.prop"
                    :slot="item.hasOther.prop"
                    :name="item.hasOther.prop"
                    :form="ruleForm" />
            </DstFormAdd>
          </template>
        </template>
      </template>
    </template>
  </el-form>
</template>

<script>
import SelectTree from '@/portal-common/components/SelectTree'
import dstUpload from '@/portal-common/components/DstUpload/index'
import render from './components/render.js'
import DstFormDetail from './components/DstFormDetail'
import DstFormAdd from './components/DstFormAdd'
const noNeedFormType = ['cutApart', 'slotMemberItem', 'slot', 'renderMemberItem', 'render', 'button'] // 不和组件内部表单有绑定关系的type类型
/**
   * 已全局注册-----关于dstForm组件的说明
   * @displayName dstForm表单组件
   */
export default {
  name: 'dstForm',
  components: { render, SelectTree, dstUpload, DstFormDetail, DstFormAdd },
  model: {
    prop: 'form',
    event: 'changeModel'
  },
  props: {
    /**
     * @model
     * v-model绑定的数据
     * @values ''
     */
    form: {
      type: Object,
      default: function () {
        return {}
      }
    },
    /**
    *表单的配置数据，后面有详细文档
    * @values '这个字段组件初始化时必填，且初始化时数组内的有值，不要初始化时传空数组，后期再往数组里加数据，这会造成一些内部监听失效'
    * @see See [当前提示链接](http://localhost:6060/dashboard#dstForm-dataArr)
    */
    dataArr: { // formOb1j
      type: Array,
      default: function () {
        return []
      }
    },
    /**
     *组件模式：取值有 detail add
     * @values detail add
    */
    modeType: {
      type: String,
      default: function () {
        return 'add'
      }
    },
    /**
     *设置表单每个元素的label宽度。也可写在dataArr内单独为单个元素设置宽度
     * @values ''
    */
    labelWidth: {
      type: String,
      default: function () {
        return '105px'
      }
    },
  },
  data() {
    return {
      ruleForm: {},
      formValidateField: {}
    }
  },
  computed: {
    // 计算出ruleForm中需要将数字转改成字符串的key值
    // 防止因为类型不对称引起的下拉框等控件的option与value对不上的情况发生
    dataArrObjKey() {
      const obj = {}
      function forKey(item, obj) {
        if (noNeedFormType.indexOf(item.type) === -1) {
          item.prop ? obj[item.prop] = item.type : ''
        }
        if (item.hasOther && Object.prototype.toString.call(item.hasOther) === '[object Object]') {
          forKey(item.hasOther, obj)
        }
      }
      this.dataArr.forEach(item => {
        forKey(item, obj)
      })
      return obj
    },
    formStr() {
      return JSON.stringify(this.form)
    },
    ruleFormStr() {
      return JSON.stringify(this.ruleForm)
    },
  },
  watch: {
    formStr: {
      deep: true,
      handler(val, oldval) {
        // 解释下为甚么这里用formStr，而不是直接用form: 因为如果直接监听form的话newVal, oldVal永远是一样的
        // 为什么永远是一样的呢？和深度无关，而是在修改（不是替换）对象或数组时，旧值将与新值相同，因为它们索引同一个对象/数组。Vue 不会保留修改之前值的副本。
        // 只有当外部得form与内部得form不一致时才执行。否则会频繁触发dom渲染。最明显得表现就是文件上传组件会一直闪
        if (!_.isEqual(this.form, this.ruleForm)) { //
          // this.setFromDataType(JSON.parse(val))
          this.setFromDataType(this.form)
        }
      }
    },
    ruleFormStr: {
      deep: true,
      handler(val, oldval) {
        // 解释下为甚么这里用ruleFormStr，而不是直接用ruleForm: 因为如果直接监听ruleForm的话newVal, oldVal永远是一样的
        // 为什么永远是一样的呢？和深度无关，而是在修改（不是替换）对象或数组时，旧值将与新值相同，因为它们索引同一个对象/数组。Vue 不会保留修改之前值的副本。
        // console.log(JSON.parse(val), JSON.parse(oldval), 1111)
        if (!_.isEqual(val, oldval)) {
          // console.log(JSON.parse(val), 1111333)
          this.setVModelEvent()
          // this.$emit('changeModel', JSON.parse(val))
        }
      }
    },
    // dataArr：
  },
  created() {
    this.init()
  },
  methods: {
    init(type) {
      const obj = {}
      this.formValidateField = {} // 前端单字段校验函数
      this.dataArr.forEach(item => {
        // 初始化form的单个字段chang后校验（为了解决某时表单框提示请填写，可是填写后红色字还不消失的情况）
        this.initValidateField(item)
        // 初始化form的初始类型值
        this.judgeType(item, obj)
      })

      let ruleForm = {}
      if (type == 'formReset') {
        // 表单重置时要以内部form权重大，跟init刚好相反
        ruleForm = {
          ...this.form,
          ...obj,
        }
      } else {
        // 初始化组件时obj和form的顺序不要调，以用户传入的值权重最大
        ruleForm = {
          ...obj,
          ...this.form
        }
      }
      // 将数字类型转成字符
      this.setFromDataType(ruleForm)
      // 执行双向绑定事件
      this.setVModelEvent()
    },
    initValidateField(item) {
      if (item.prop) {
        this.formValidateField[item.prop] = _.debounce(() => {
          this.$refs.ruleForm.validateField(item.prop)
        }, 1000)
      }
      if (item.hasOther && Object.prototype.toString.call(item.hasOther) === '[object Object]' && item.hasOther.prop) {
        this.initValidateField(item.hasOther)
      }
    },
    // 将外部传入的表单数据是数字类型的，转成字符串儿类型
    setFromDataType(newVal) {
      for (const key in newVal) {
        // 将与表单绑定的值取出，如果是数字转成字符串，防止下拉框的option与value对不上的情况发生
        // 注意newVal[key]内只能将普通类型重新赋值，不能将数组或对象重新赋值，会报死循环
        if (this.dataArrObjKey[key] && Object.prototype.toString.call(newVal[key]) === '[object Number]') {
          newVal[key] += ''
        } else if (this.dataArrObjKey[key] && Object.prototype.toString.call(newVal[key]) === '[object Array]') {
          newVal[key].forEach((item, index) => {
            if (Object.prototype.toString.call(item) === '[object Number]') {
              newVal[key][index] = (item += '')
            }
          })
        }
      }
      // console.log(44444)
      // this.ruleForm = _.cloneDeep(newVal)
      this.ruleForm = Object.assign({}, this.form, newVal)
    },
    calcDefaultOption(item, type, defalut = true) {
      return (item[type] !== undefined) ? item[type] : defalut
    },
    // 初始化form的初始类型值
    judgeType(item, obj) {
      const initArrType = ['dstUpload', 'checkbox', 'cascader'] // 需要默认为数组类型的type类型
      if (item.initValue) {
        obj[item.prop] = item.initValue
      } else if (initArrType.includes(item.type)) {
        obj[item.prop] = []
      } else if (['select'].includes(item.type) && item.multiple) {
        obj[item.prop] = []
      } else if (!noNeedFormType.includes(item.type)) {
        obj[item.prop] = ''
      }
      if (item.hasOther && Object.prototype.toString.call(item.hasOther) === '[object Object]') {
        this.judgeType(item.hasOther, obj)
      }
    },
    cascaderItemChange(val, item) {
      // 当父级选项变化时触发的事件，仅在 change-on-select 为 false 时可用
      // console.log(item, 9988)
      if (item.handleItemChange) {
        item.handleItemChange(val)
      }
      if (item.activeItemChange) {
        item.activeItemChange(val)
      }
      /**
      * 级联选择器的change事件
      * @property {({prop,value,ruleForm})}
      * @property {prop} 当前key
      * @property {value} 当前的key对应的值
      * @property {ruleForm} 内部的整个form
      */
      this.$emit('activeItemChange', {
        prop: item.prop,
        value: this.ruleForm[item.prop],
        ruleForm: this.ruleForm
      })
      /**
      * 级联选择器的change事件
      * @ignore 该事件不需要生成文档，同上方的activeItemChange为同一个事件
      * @property {({prop,value,ruleForm})}
      * @property {prop} 当前key
      * @property {value} 当前的key对应的值
      * @property {ruleForm} 内部的整个form
      */
      this.$emit('cascaderItemChange', {
        prop: item.prop,
        value: this.ruleForm[item.prop],
        ruleForm: this.ruleForm
      })
    },
    itemClick(item, index) {
      // 若有自定义事件，此时调用
      if (item.click) {
        item.click(this.ruleForm[item.prop] || '', item, index)
      }
      /**
     * 某一项的点击事件
     * @property {({prop,value,ruleForm})}
     * @property {prop} 当前key
     * @property {value} 当前的key对应的值
     * @property {ruleForm} 内部的整个form
     */
      this.$emit('itemClick', {
        prop: item.prop,
        value: this.ruleForm[item.prop],
        ruleForm: this.ruleForm
      })
    },
    selectCascaderChange(item, arr, currentLabels) {
      let key = 'value'
      let children = 'children'
      if (item.props && item.props.value) {
        key = item.props.value
      }
      if (item.props && item.props.children) {
        children = item.props.children
      }
      let selectArr = []
      if (item.options && arr.length) {
        selectArr = this.getChain({
          value: arr[arr.length - 1],
          data: item.options,
          props: { id: key, children: children }
        })
      }
      this.itemChange(item, selectArr, currentLabels)
    },
    selectItemChange(item, id) {
      let key = 'value'
      if (item.props && item.props.value) {
        key = item.props.value
      }
      let selectData = null
      if (item.options && (id || id === 0)) {
        if (Object.prototype.toString.call(id) === '[object Array]' && id.length) {
          selectData = []
          id.forEach(value => {
            selectData.push(item.options.find(res => res[key] == value))
          })
        } else {
          selectData = item.options.find(res => res[key] == id)
        }
      }
      this.itemChange(item, selectData)
    },
    itemChange(item, ...otherParams) { // otherParams指的是组件本身的回调参数
      // 记得在chang的第一时间触发changeModel事件，以达到外部的form能及时双向绑定成功。否则若用户此时在外部chang回调中修改form值后内部再次拿着外部并不是最新的值而对内部修改，那将遗漏form最新的数据bug
      this.setVModelEvent()
      // 若有自定义change事件，此时调用
      if (item.change) {
        // let optionObj =
        item.change(this.ruleForm[item.prop], otherParams)
      }
      /**
       * 某一项的Change事件
       * @property {({prop,value,ruleForm,otherParams})}
       * @property {prop} 当前key
       * @property {value} 当前的key对应的值
       * @property {ruleForm} 内部的整个form
       * @property {otherParams} 单个组件改变时当前这个组件的内部回调数据，比如slect组件chang时，会将当前被选中那一项的option-item放进otherParams中一起传回，这个非常有用，当我们选择了一个id后，后端常常希望前端同时传回对应的name，这是可在otherParams中直接读取对应的值
       */
      this.$emit('itemChange', {
        prop: item.prop,
        value: this.ruleForm[item.prop],
        ruleForm: this.ruleForm,
        otherParams
      })
      // 对该字段校验（去错误提示），解决会出现某种情况下明明选项里有值，却还是提示未填入红字提示
      // console.log(this.$refs['ruleForm'].validateField(item.prop), 55555)
      if (this.formValidateField[item.prop]) {
        this.formValidateField[item.prop]()
      }
      // this.$refs['ruleForm'].validateField(item.prop)
    },
    // 根据底层子ID获取级联,
    // 使用场景：层级联动使用，根据最后一级的id获取所有父级级联的对象
    getChain(opt = {}) {
      const {
        data = [],
        props = { id: 'id', children: 'children' },
        isStrictSquals = false, // 是否要严格全等模式
        value
      } = opt
      const result = []
      function searching(id, data) {
        // debugger
        for (const i in data) {
          const item = data[i]
          const child =
            Object.prototype.toString.call(item[props.children]) ===
              '[object Array]'
              ? item[props.children]
              : []
          result.push(item)
          if (
            (isStrictSquals && id === item[props.id]) ||
            (!isStrictSquals && id == item[props.id])
          ) {
            return true
          } else if (child.length && searching(id, child)) {
            return true
          }
          result.pop()
        }
      }
      if (Object.prototype.toString.call(data) === '[object Array]' && data.length) {
        searching(value, data)
      }
      return result
    },
    /**
     * 校验表单的值是否符合rule规则
    * @public 向外暴露的函数
    * @param {(valid)} 校验是否通过，在回调函数的参数中传回
    */
    formValidate(fn) {
      if (this.$refs.DstFormAdd) {
        for (let i = 0; i < this.$refs.DstFormAdd.length; i++) {
          if (this.$refs.DstFormAdd[i].getIsUploading()) {
            fn(false)
            return
          }
        }
      }
      this.$refs.ruleForm.validate(fn)
    },
    /**
     * 重置表单的值
    * @public 向外暴露的函数
    */
    formReset() {
      this.$refs.ruleForm.resetFields()
      // init一下是为了初始化数据的默认值，比如有些值应该是数组,但是由于是重置操作，变成了字符类型，会报错
      this.init('formReset')
    },
    /**
    * 移除该表单项的校验结果
   * @public 向外暴露的函数
   */
    clearValidate() {
      this.$refs.ruleForm.clearValidate()
    },
    // 已废弃，请勿再用，获取内部表单值
    getValue() {
      return this.ruleForm
    },
    //  已废弃，请勿再用，设置某个单项的表单值
    setValue(prop, value = '') {
      this.ruleForm[prop] = value
      this.setVModelEvent()
    },
    // 附件上传成功回调
    fileOnSuccess(item, params) {
      if (item.onSuccess) {
        item.onSuccess(item, ...params)
      }
    },
    // 附件上传失败回调
    fileOnError(item, params) {
      if (item.onError) {
        item.onError(item, ...params)
      }
    },
    // 不修改form对象的地址，将内部的ruleForm值同样赋值给form
    initSetOuterForm() {
      Object.keys(this.ruleForm).forEach(key => {
        // this.form[key] = this.ruleForm[key]
        this.$set(this.form, key, this.ruleForm[key])
      })
    },
    // 出发双向绑定事件，传输数据
    setVModelEvent(first) {
      // 千万注意：解决初始化多个表单组件绑定同一个对象时数据同步的问题,所以必须要emit出去this.form，要是clone一个新对象，多个表单时this.ruleForm初始的字段就不能同步,但是如果直接emit出去this.form的话又不能做到实时同步更新，真是哎。。。
      // 如果直接emit出去this.form的话，由于vue的底层渲染特性只能对父级页面上ruleForm里有初始化过的字段进行双向绑定。没有初始化的就不能实时更新，除非$emit出去一个全新的对象，但是emit全新的对象又与多个表单组件绑定同一个对象时数据同步的问题冲突了,这可真是愁死我了哟
      // 解决思路，可以用$set主动让form将组件内自动生成的字段也被vue监听到
      this.initSetOuterForm() // emit时，如果是this.form则务必记得打开这行
      /**
       * 触发双向绑定的事件
       * @property {(form)}
       * @property {form} 内部的整个form
       */
      this.$emit('changeModel', this.form) // 这样写好像不能实时同步更新
      // if (first) {
      //   this.initSetOuterForm() // emit时，如果是this.form则务必记得打开这行
      //   this.$emit('changeModel', this.form) // 这样写好像不能实时同步更新
      // } else {
      //   this.$emit('changeModel', Object.assign({}, this.form, this.ruleForm))
      // }
      // this.$emit('changeModel', _.cloneDeep(this.form)) // (深拷贝)这样写输入数据时，其他表单模块的文件组件会闪一下
      // this.$emit('changeModel', Object.assign({}, this.form, this.ruleForm)) // (浅拷贝)这样写输入数据时其他表单模块的文件组件会闪一下,（摸索中：好像是如果添加了change事件的字段发生变化的时候，就会引起文件组件闪一下）
      // console.log(_.cloneDeep(Object.assign(this.form, this.ruleForm)), 11111)
      // 规律：1、在itemChange函数中，如果有注册change事件的字段，那么该字段改变时就会触发图片闪烁。如果注释掉change那行代码测试在该情况下就不会出现，but不能注释
      // 2、同一页面上多个表单模块共用v-model一个ruleform对象时，A模块中有图片组件，B模板中任意字段改变时，不管有没有注册change事件都会触发A模块图片闪烁
      // 感觉就是不能changeModel一个完全新的对象，但是不这样又会引发同一个对象改变后外部表单无法感知的情况
      // 以上两种问题目前已修复，修复方式是将：
      // 1、setFromDataType函数中最后对this.ruleForm = _.cloneDeep(newVal)改成了this.ruleForm = Object.assign(this.form, newVal)
      // 2、init函数中 this.ruleForm = {...obj, ...this.form };this.setFromDataType(this.ruleForm)改成了const ruleForm = {...obj, ...this.form };this.setFromDataType(ruleForm)
      // 目前测试了几个页面没有问题，功能都正常
    },
  }
}
</script>
<style lang="scss" scoped>
/deep/.el-col {
  height: 58px;
}

/deep/.click-href {
  cursor: pointer;
  white-space: normal;
  word-break: break-all;
  color: #00a47e;
}
</style>

<style lang="scss" >
.components-myForm-wrap {
  .dst-add-input {
    word-wrap: break-word;
    word-break: normal;
  }
  .dst-add-form-item .el-form-item__content .dst-add-input.fileBtn-wrap {
    max-width: 100%;
  }
  .cutApart {
    width: 100%;
    padding: 20px;
  }
  .el-select__tags {
    top: 3%;
    -webkit-transform: translateY(0%);
    transform: translateY(0%);
  }
  .hasOther-left-margin {
    margin-left: 5px;
  }
  .el-button--text.is-plain:hover,
  .el-button--text.is-plain:focus {
    border: none;
  }
  .el-form-item.hasOther-el-form-item {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 auto;
    flex: 0 0 auto;
    min-width: auto;
    margin: 0 0 2px 5px;
    padding: 0;
    width: auto;
  }
}
.hasOther-el-form-item.el-form-item .el-form-item__content {
  height: 100%;
  display: flex;
  align-items: center;
}
.el-form-item.is-error .el-input__inner {
  border-color: #f56c6c;
}
// 详情模式下样式
.components-myForm-wrap.detail-mode-type {
  // 详情模式下间距不需要太高
  &.dst-add-item-box .el-form-item {
    margin-bottom: 5px;
  }
  // 详情模式下不需要必填*
  .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label:before {
    display: none;
  }
  // 详情模式下，一行有两个input等元素时根据内容自动撑开，不需要设置最小宽度
  .dst-add-form-item .el-form-item__content .dst-add-input {
    flex: 0 0 auto;
    min-width: auto;
    margin-right: 10px;
  }
}
</style>
